package z1log

import (
	"fmt"
	"io"
	"log"
	"os"
	"path"
	"path/filepath"
	"strings"
	"sync"
	"time"
)

// SetLog(DEBUG, "logs/app.log", EveryYear, true) // Output to files and console, split files by date and log level
// SetLog(DEBUG, "logs/app.log", EveryYear, false)	// Output to files and console, only split files by date
// SetLog(DEBUG, "", EveryYear, false) // Only output to the console
func SetLog(level int, fileName string, splitTime int, splitLevel bool, outputType int) {
	prefixStr := `,1,2,3,`
	if !strings.Contains(prefixStr, fmt.Sprintf(`,%v,`, outputType)) {
		__outputType = OUTPUT_TYPE_STDOUT
	} else {
		__outputType = outputType
	}

	z1Logger.Close()
	z1Logger = NewLogger(level, fileName, splitTime, splitLevel)
}
func Debug(message ...any) {
	z1Logger.Println(DEBUG, message...)
}
func Info(message ...any) {
	z1Logger.Println(INFO, message...)
}
func Warning(message ...any) {
	z1Logger.Println(WARNING, message...)
}
func Error(message ...any) {
	z1Logger.Println(ERROR, message...)
}
func Debugf(format string, message ...any) {
	z1Logger.Printf(DEBUG, format, message...)
}
func Infof(format string, message ...any) {
	z1Logger.Printf(INFO, format, message...)
}
func Warningf(format string, message ...any) {
	z1Logger.Printf(WARNING, format, message...)
}
func Errorf(format string, message ...any) {
	z1Logger.Printf(ERROR, format, message...)
}

func init() {
	z1Logger = NewLogger(DEBUG, "", EveryDay, true)
}

// ---------------------------internal--------------------------

var z1Logger *Logger
var closeLogger = make(chan struct{}, 1)
var __outputType int = OUTPUT_TYPE_STDOUT

type Logger struct {
	debugFile     *os.File
	infoFile      *os.File
	warningFile   *os.File
	errorFile     *os.File
	debugLogger   *log.Logger
	infoLogger    *log.Logger
	warningLogger *log.Logger
	errorLogger   *log.Logger
	level         int
	mutextMap     map[int]*sync.Mutex
	loggerMap     map[int]*log.Logger
	splitLevel    bool
	fileName      string
}

const (
	DEBUG = iota + 1
	INFO
	WARNING
	ERROR
	ALL = 735
)

const (
	EveryYear = iota + 1
	EveryMonth
	EveryDay
	EveryWeek
	EveryHour
	EveryMinute
)

const (
	OUTPUT_TYPE_STDOUT = iota + 1
	OUTPUT_TYPE_FILE
	OUTPUT_TYPE_BOTH
)

func NewLogger(level int, fileName string, splitTime int, splitLevel bool) *Logger {
	logger := &Logger{
		level:      level,
		splitLevel: splitLevel,
		fileName:   fileName,
	}

	logger.mutextMap = map[int]*sync.Mutex{
		DEBUG:   &sync.Mutex{},
		INFO:    &sync.Mutex{},
		WARNING: &sync.Mutex{},
		ERROR:   &sync.Mutex{},
		ALL:     &sync.Mutex{},
	}

	logger.setLogFiles(fileName, splitTime, splitLevel)

	// if fileName != `` {
	if __outputType != OUTPUT_TYPE_STDOUT {
		go logger.periodicLogRotation(fileName, splitTime, splitLevel)
	}

	logger.loggerMap = map[int]*log.Logger{
		DEBUG:   logger.debugLogger,
		INFO:    logger.infoLogger,
		WARNING: logger.warningLogger,
		ERROR:   logger.errorLogger,
	}

	return logger
}

func (l *Logger) getDateStr(splitTime int) (dateStr string) {
	currentTime := time.Now()

	switch splitTime {
	case EveryYear:
		return currentTime.Format("2006")
	case EveryMonth:
		return currentTime.Format("2006-01")
	case EveryDay:
		return currentTime.Format("2006-01-02")
	case EveryHour:
		return currentTime.Format("2006-01-02_15")
	case EveryMinute:
		return currentTime.Format("2006-01-02_15_04")
	case EveryWeek:
		year, week := currentTime.ISOWeek()
		return fmt.Sprintf(`%v_%vweek`, year, week)
	}

	return

}
func (l *Logger) setLogFiles(filePath string, splitTime int, splitLevel bool) {

	flag := log.Ldate | log.Lmicroseconds | log.Lshortfile

	if l.debugLogger == nil {
		l.debugLogger = log.New(io.MultiWriter(os.Stdout), "[DEBUG] ", flag)
		l.infoLogger = log.New(io.MultiWriter(os.Stdout), "[INFO] ", flag)
		l.warningLogger = log.New(io.MultiWriter(os.Stdout), "[WARNING] ", flag)
		l.errorLogger = log.New(io.MultiWriter(os.Stdout), "[ERROR] ", flag)
	}

	// if filePath == `` {
	if __outputType == OUTPUT_TYPE_STDOUT {
		return
	}

	date := l.getDateStr(splitTime)

	dir := filepath.Dir(filePath)
	fileName := filepath.Base(filePath)
	ext := filepath.Ext(fileName)
	name := strings.TrimSuffix(fileName, ext)
	// log.Println(dir, fileName, name, ext)

	os.MkdirAll(dir, 0666)

	logFileName := path.Join(dir, name+"_"+date+".log")
	logHandle, _ := os.OpenFile(logFileName, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)

	l.debugFile = logHandle
	l.infoFile = logHandle
	l.warningFile = logHandle
	l.errorFile = logHandle

	if splitLevel {
		debugFileName := path.Join(dir, name+"_debug_"+date+".log")
		infoFileName := path.Join(dir, name+"_info_"+date+".log")
		warningFileName := path.Join(dir, name+"_warning_"+date+".log")
		errorFileName := path.Join(dir, name+"_error_"+date+".log")

		debugHandle, _ := os.OpenFile(debugFileName, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
		infoHandle, _ := os.OpenFile(infoFileName, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
		warningHandle, _ := os.OpenFile(warningFileName, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
		errorHandle, _ := os.OpenFile(errorFileName, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)

		l.debugFile = debugHandle
		l.infoFile = infoHandle
		l.warningFile = warningHandle
		l.errorFile = errorHandle
	}

	if __outputType == OUTPUT_TYPE_FILE {
		l.debugLogger.SetOutput(io.MultiWriter(l.debugFile))
		l.infoLogger.SetOutput(io.MultiWriter(l.infoFile))
		l.warningLogger.SetOutput(io.MultiWriter(l.warningFile))
		l.errorLogger.SetOutput(io.MultiWriter(l.errorFile))
	} else if __outputType == OUTPUT_TYPE_BOTH {
		l.debugLogger.SetOutput(io.MultiWriter(l.debugFile, os.Stdout))
		l.infoLogger.SetOutput(io.MultiWriter(l.infoFile, os.Stdout))
		l.warningLogger.SetOutput(io.MultiWriter(l.warningFile, os.Stdout))
		l.errorLogger.SetOutput(io.MultiWriter(l.errorFile, os.Stdout))
	}
}

func (l *Logger) periodicLogRotation(filePath string, splitTime int, splitLevel bool) {
	ticker := time.NewTicker(1 * time.Minute)
	for {
		select {
		case <-closeLogger:
			// log.Println(`----in periodicLogRotation---closeLogger--`, filePath)
			return
		case <-ticker.C:
			// log.Println(`----in periodicLogRotation-----`, filePath)
			l.checkLogFile(filePath, splitTime, splitLevel)
		}
	}
}

func (l *Logger) checkLogFile(filePath string, splitTime int, splitLevel bool) {
	// currentTime := time.Now()
	// date := currentTime.Format("2006-01-02")
	// date := dateStr
	date := l.getDateStr(splitTime)
	dir := filepath.Dir(filePath)
	fileName := filepath.Base(filePath)
	ext := filepath.Ext(fileName)
	name := strings.TrimSuffix(fileName, ext)
	debugFileNameNew := path.Join(dir, name+"_debug_"+date+".log")
	debugFileName := l.debugFile.Name()

	if debugFileNameNew != debugFileName {
		debugFileOld := l.debugFile
		infoFileOld := l.infoFile
		warningFileOld := l.warningFile
		errorFileOld := l.errorFile

		l.setLogFiles(filePath, splitTime, splitLevel)

		debugFileOld.Close()
		infoFileOld.Close()
		warningFileOld.Close()
		errorFileOld.Close()
	}
}

func (l *Logger) Println(level int, message ...any) {
	if l.level <= level {
		l.mutextMap[level].Lock()
		defer l.mutextMap[level].Unlock()
		// l.loggerMap[level].Println(message...)
		// https://darjun.github.io/2020/02/07/godailylib/log/#%E5%AE%9E%E7%8E%B0
		l.loggerMap[level].Output(3, fmt.Sprintln(message...))
	}
}
func (l *Logger) Printf(level int, format string, message ...any) {
	if l.level <= level {
		mu := l.mutextMap[level]
		if !l.splitLevel {
			mu = l.mutextMap[ALL]
		}
		mu.Lock()
		defer mu.Unlock()
		// (l.loggerMap[level]).Printf(format, message...)
		l.loggerMap[level].Output(3, fmt.Sprintf(format, message...))
	}
}

func (l *Logger) Close() {
	if l.fileName != `` {
		closeLogger <- struct{}{}
	}

	l.debugFile.Close()
	l.infoFile.Close()
	l.warningFile.Close()
	l.errorFile.Close()
}
